import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';

const _kIsWeb = bool.hasEnvironment('dart.library.js_util')
    ? bool.fromEnvironment('dart.library.js_util')
    : identical(0, 0.0);

/// - `(?<=)` is a positive lookbehind assertion that matches a comma (",")
/// only if it's preceded by a specific pattern. In this case, the lookbehind
/// assertion is empty, which means it matches any comma that's preceded by any character.
/// - `(,)` captures the comma as a group.
/// - `(?=[^;]+?=)` is a positive lookahead assertion that matches a comma only
/// if it's followed by a specific pattern. In this case, it matches any comma
/// that's followed by one or more characters that are not semicolons (";") and
/// then an equals sign ("="). This ensures that the comma is not part of a cookie
/// attribute like "expires=Sun, 19 Feb 3000 01:43:15 GMT", which could also contain commas.
final _setCookieReg = RegExp('(?<=)(,)(?=[^;]+?=)');

/// Cookie manager for HTTP requests based on [CookieJar].
class MyCookieManager extends Interceptor {
  const MyCookieManager(
    this.cookieJar,
  ) : assert(!_kIsWeb, "Don't use the manager in Web environments.");

  final CookieJar cookieJar;

  /// Merge cookies into a Cookie string.
  /// Cookies with longer paths are listed before cookies with shorter paths.
  static String getCookies(List<Cookie> cookies) {
    // Sort cookies by path (longer path first).
    cookies.sort((a, b) {
      if (a.path == null && b.path == null) {
        return 0;
      } else if (a.path == null) {
        return -1;
      } else if (b.path == null) {
        return 1;
      } else {
        return b.path!.length.compareTo(a.path!.length);
      }
    });
    return cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
  }

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    cookieJar.loadForRequest(options.uri).then((cookies) {
      final previousCookies =
          options.headers[HttpHeaders.cookieHeader] as String?;
      final newCookies = getCookies([
        ...?previousCookies
            ?.split(';')
            .where((e) => e.isNotEmpty)
            .map((c) => Cookie.fromSetCookieValue(c)),
        ...cookies,
      ]);
      options.headers[HttpHeaders.cookieHeader] =
          newCookies.isNotEmpty ? newCookies : null;
      handler.next(options);
    }).catchError((dynamic e, StackTrace s) {
      final error = DioException(
        requestOptions: options,
        error: e,
        stackTrace: s,
      );
      handler.reject(error, true);
    });
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    _saveCookies(response).then((_) => handler.next(response)).catchError(
      (dynamic e, StackTrace s) {
        final error = DioException(
          requestOptions: response.requestOptions,
          error: e,
          stackTrace: s,
        );
        handler.reject(error, true);
      },
    );
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    if (err.response != null) {
      _saveCookies(err.response!).then((_) => handler.next(err)).catchError(
        (dynamic e, StackTrace s) {
          final error = DioException(
            requestOptions: err.response!.requestOptions,
            error: e,
            stackTrace: s,
          );
          handler.next(error);
        },
      );
    } else {
      handler.next(err);
    }
  }

  Future<void> _saveCookies(Response response) async {
    final setCookies = response.headers[HttpHeaders.setCookieHeader];
    if (setCookies == null || setCookies.isEmpty) {
      return;
    }
    final List<Cookie> cookies = setCookies
        .map((str) => str.split(_setCookieReg))
        .expand((cookie) => cookie)
        .where((cookie) => cookie.isNotEmpty)
        .map((str) => _Cookie.fromSetCookieValue(str))
        .toList();
    // Handle `Set-Cookie` when `followRedirects` is false
    // and the response returns a redirect status code.
    final statusCode = response.statusCode ?? 0;
    // 300 indicates the URL has multiple choices, so here we use list literal.
    final locations = response.headers[HttpHeaders.locationHeader] ?? [];
    // We don't want to explicitly consider recursive redirections
    // cookie handling here, because when `followRedirects` is set to false,
    // users will be available to handle cookies themselves.
    final isRedirectRequest = statusCode >= 300 && statusCode < 400;
    // Saving cookies for the original site.
    // Spec: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.2.
    final originalUri = response.requestOptions.uri;
    final realUri = originalUri.resolveUri(response.realUri);
    await cookieJar.saveFromResponse(realUri, cookies);
    if (isRedirectRequest && locations.isNotEmpty) {
      final originalUri = response.realUri;
      await Future.wait(
        locations.map(
          (location) => cookieJar.saveFromResponse(
            // Resolves the location based on the current Uri.
            originalUri.resolve(location),
            cookies,
          ),
        ),
      );
    }
  }

}


class _Cookie implements Cookie {
  String _name;
  String _value;
  DateTime? expires;
  int? maxAge;
  String? domain;
  String? _path;
  bool httpOnly = false;
  bool secure = false;
  SameSite? sameSite;

  _Cookie(String name, String value)
      : _name = _validateName(name),
        _value = _validateValue(value),
        httpOnly = true;

  String get name => _name;
  String get value => _value;

  String? get path => _path;

  set path(String? newPath) {
    _validatePath(newPath);
    _path = newPath;
  }

  set name(String newName) {
    _validateName(newName);
    _name = newName;
  }

  set value(String newValue) {
    _validateValue(newValue);
    _value = newValue;
  }

  _Cookie.fromSetCookieValue(String value)
      : _name = "",
        _value = "" {
    // Parse the 'set-cookie' header value.
    _parseSetCookieValue(value);
  }

  // Parse a 'set-cookie' header value according to the rules in RFC 6265.
  void _parseSetCookieValue(String s) {
    int index = 0;

    bool done() => index == s.length;

    String parseName() {
      int start = index;
      while (!done()) {
        if (s[index] == "=") break;
        index++;
      }
      return s.substring(start, index).trim();
    }

    String parseValue() {
      int start = index;
      while (!done()) {
        if (s[index] == ";") break;
        index++;
      }
      return s.substring(start, index).trim();
    }

    void parseAttributes() {
      String parseAttributeName() {
        int start = index;
        while (!done()) {
          if (s[index] == "=" || s[index] == ";") break;
          index++;
        }
        return s.substring(start, index).trim().toLowerCase();
      }

      String parseAttributeValue() {
        int start = index;
        while (!done()) {
          if (s[index] == ";") break;
          index++;
        }
        return s.substring(start, index).trim().toLowerCase();
      }

      while (!done()) {
        String name = parseAttributeName();
        String value = "";
        if (!done() && s[index] == "=") {
          index++; // Skip the = character.
          value = parseAttributeValue();
        }
        if (name == "expires") {
          expires = _parseCookieDate(value);
        } else if (name == "max-age") {
          maxAge = int.parse(value);
        } else if (name == "domain") {
          domain = value;
        } else if (name == "path") {
          path = value;
        } else if (name == "httponly") {
          httpOnly = true;
        } else if (name == "secure") {
          secure = true;
        } else if (name == "samesite") {
          sameSite = switch (value) {
            "lax" => SameSite.lax,
            "none" => SameSite.none,
            "strict" => SameSite.strict,
            _ => throw HttpException(
                'SameSite value should be one of Lax, Strict or None.')
          };
        }
        if (!done()) index++; // Skip the ; character
      }
    }

    _name = _validateName(parseName());
    if (done() || _name.isEmpty) {
      throw HttpException("Failed to parse header value [$s]");
    }
    index++; // Skip the = character.
    _value = _validateValue(parseValue());
    if (done()) return;
    index++; // Skip the ; character.
    parseAttributes();
  }

  String toString() {
    StringBuffer sb = StringBuffer();
    sb
      ..write(_name)
      ..write("=")
      ..write(_value);
    var expires = this.expires;
    if (expires != null) {
      sb
        ..write("; Expires=")
        ..write(HttpDate.format(expires));
    }
    if (maxAge != null) {
      sb
        ..write("; Max-Age=")
        ..write(maxAge);
    }
    if (domain != null) {
      sb
        ..write("; Domain=")
        ..write(domain);
    }
    if (path != null) {
      sb
        ..write("; Path=")
        ..write(path);
    }
    if (secure) sb.write("; Secure");
    if (httpOnly) sb.write("; HttpOnly");
    if (sameSite != null) sb.write("; $sameSite");

    return sb.toString();
  }

  static String _validateName(String newName) {
    const separators = [
      "(",
      ")",
      "<",
      ">",
      "@",
      ",",
      ";",
      ":",
      "\\",
      '"',
      "/",
      "?",
      "=",
      "{",
      "}"
    ];
    if (newName == null) throw ArgumentError.notNull("name");
    for (int i = 0; i < newName.length; i++) {
      int codeUnit = newName.codeUnitAt(i);
      if (codeUnit <= 32 ||
          codeUnit >= 127 ||
          separators.contains(newName[i])) {
        throw FormatException(
            "Invalid character in cookie name, code unit: '$codeUnit'",
            newName,
            i);
      }
    }
    return newName;
  }

  static String _validateValue(String newValue) {
    if (newValue == null) throw ArgumentError.notNull("value");
    // Per RFC 6265, consider surrounding "" as part of the value, but otherwise
    // double quotes are not allowed.
    int start = 0;
    int end = newValue.length;
    if (2 <= newValue.length &&
        newValue.codeUnits[start] == 0x22 &&
        newValue.codeUnits[end - 1] == 0x22) {
      start++;
      end--;
    }

    for (int i = start; i < end; i++) {
      int codeUnit = newValue.codeUnits[i];
      if (!(codeUnit == 0x21 ||
          (codeUnit >= 0x23 && codeUnit <= 0x2B) ||
          (codeUnit >= 0x2D && codeUnit <= 0x3A) ||
          (codeUnit >= 0x3C && codeUnit <= 0x5B) ||
          (codeUnit >= 0x5D && codeUnit <= 0x7E))) {
        throw FormatException(
            "Invalid character in cookie value, code unit: '$codeUnit'",
            newValue,
            i);
      }
    }
    return newValue;
  }

  static void _validatePath(String? path) {
    if (path == null) return;
    for (int i = 0; i < path.length; i++) {
      int codeUnit = path.codeUnitAt(i);
      // According to RFC 6265, semicolon and controls should not occur in the
      // path.
      // path-value = <any CHAR except CTLs or ";">
      // CTLs = %x00-1F / %x7F
      if (codeUnit < 0x20 || codeUnit >= 0x7f || codeUnit == 0x3b /*;*/) {
        throw FormatException(
            "Invalid character in cookie path, code unit: '$codeUnit'");
      }
    }
  }

  static DateTime _parseCookieDate(String date) {
    const List monthsLowerCase = [
      "jan",
      "feb",
      "mar",
      "apr",
      "may",
      "jun",
      "jul",
      "aug",
      "sep",
      "oct",
      "nov",
      "dec"
    ];

    int position = 0;

    Never error() {
      throw HttpException("Invalid cookie date $date");
    }

    bool isEnd() => position == date.length;

    bool isDelimiter(String s) {
      int char = s.codeUnitAt(0);
      if (char == 0x09) return true;
      if (char >= 0x20 && char <= 0x2F) return true;
      if (char >= 0x3B && char <= 0x40) return true;
      if (char >= 0x5B && char <= 0x60) return true;
      if (char >= 0x7B && char <= 0x7E) return true;
      return false;
    }

    bool isNonDelimiter(String s) {
      int char = s.codeUnitAt(0);
      if (char >= 0x00 && char <= 0x08) return true;
      if (char >= 0x0A && char <= 0x1F) return true;
      if (char >= 0x30 && char <= 0x39) return true; // Digit
      if (char == 0x3A) return true; // ':'
      if (char >= 0x41 && char <= 0x5A) return true; // Alpha
      if (char >= 0x61 && char <= 0x7A) return true; // Alpha
      if (char >= 0x7F && char <= 0xFF) return true; // Alpha
      return false;
    }

    bool isDigit(String s) {
      int char = s.codeUnitAt(0);
      if (char > 0x2F && char < 0x3A) return true;
      return false;
    }

    int getMonth(String month) {
      if (month.length < 3) return -1;
      return monthsLowerCase.indexOf(month.substring(0, 3));
    }

    int toInt(String s) {
      int index = 0;
      for (; index < s.length && isDigit(s[index]); index++);
      return int.parse(s.substring(0, index));
    }

    var tokens = <String>[];
    while (!isEnd()) {
      while (!isEnd() && isDelimiter(date[position])) position++;
      int start = position;
      while (!isEnd() && isNonDelimiter(date[position])) position++;
      tokens.add(date.substring(start, position).toLowerCase());
      while (!isEnd() && isDelimiter(date[position])) position++;
    }

    String? timeStr;
    String? dayOfMonthStr;
    String? monthStr;
    String? yearStr;

    for (var token in tokens) {
      if (token.isEmpty) continue;
      if (timeStr == null &&
          token.length >= 5 &&
          isDigit(token[0]) &&
          (token[1] == ":" || (isDigit(token[1]) && token[2] == ":"))) {
        timeStr = token;
      } else if (dayOfMonthStr == null && isDigit(token[0])) {
        dayOfMonthStr = token;
      } else if (monthStr == null && getMonth(token) >= 0) {
        monthStr = token;
      } else if (yearStr == null &&
          token.length >= 2 &&
          isDigit(token[0]) &&
          isDigit(token[1])) {
        yearStr = token;
      }
    }

    if (timeStr == null ||
        dayOfMonthStr == null ||
        monthStr == null ||
        yearStr == null) {
      error();
    }

    int year = toInt(yearStr);
    if (year >= 70 && year <= 99)
      year += 1900;
    else if (year >= 0 && year <= 69) year += 2000;
    if (year < 1601) error();

    int dayOfMonth = toInt(dayOfMonthStr);
    if (dayOfMonth < 1 || dayOfMonth > 31) error();

    int month = getMonth(monthStr) + 1;

    var timeList = timeStr.split(":");
    if (timeList.length != 3) error();
    int hour = toInt(timeList[0]);
    int minute = toInt(timeList[1]);
    int second = toInt(timeList[2]);
    if (hour > 23) error();
    if (minute > 59) error();
    if (second > 59) error();

    return DateTime.utc(year, month, dayOfMonth, hour, minute, second, 0);
  }
}

class MyPersistCookieJar extends DefaultCookieJar {
  /// [persistSession] is whether persisting the cookies that without
  /// "expires" or "max-age" attribute.
  /// If tbe value is false, the session cookies will be discarded.
  /// Otherwise, the session cookies will be persisted.
  ///
  /// [ignoreExpires] will determine whether to save/load expired cookies.
  ///
  /// [storage] is defaults to [FileStorage].
  MyPersistCookieJar({
    this.persistSession = true,
    bool ignoreExpires = false,
    Storage? storage,
    this.deleteHostCookiesWhenLoadFailed = true,
  })  : storage = storage ?? FileStorage(),
        super(ignoreExpires: ignoreExpires);

  /// Whether persisting cookies without "expires" or "max-age" attribute.
  final bool persistSession;
  final Storage storage;
  final bool deleteHostCookiesWhenLoadFailed;

  static const _indexKey = '.index';
  static const _domainsKey = '.domains';

  Set<String> _hostSet = {};

  bool _initialized = false;

  Future<void> forceInit() => _checkInitialized(force: true);

  Future<void> _checkInitialized({bool force = false}) async {
    if (!force && _initialized) {
      return;
    }
    await storage.init(persistSession, ignoreExpires);
    // Load domain cookies.
    String? str = await storage.read(_domainsKey);
    if (str != null && str.isNotEmpty) {
      try {
        final Map<String, dynamic> jsonData = json.decode(str);
        final Map<String, Map<String, Map<String, SerializableCookie>>>
        cookies = jsonData.map((domain, cookies) {
          final domainCookies = cookies
              .cast<String, dynamic>()
              .map<String, Map<String, SerializableCookie>>(
                (path, map) {
              final cookieForPath = map.cast<String, String>();
              final realCookies =
              cookieForPath.map<String, SerializableCookie>((
                  String cookieName,
                  String cookie,
                  ) {
                return MapEntry<String, SerializableCookie>(
                  cookieName,
                  SerializableCookie.fromJson(cookie),
                );
              });
              return MapEntry<String, Map<String, SerializableCookie>>(
                path,
                realCookies,
              );
            },
          );
          return MapEntry<String, Map<String, Map<String, SerializableCookie>>>(
            domain,
            domainCookies,
          );
        });
        domainCookies
          ..clear()
          ..addAll(cookies);
      } catch (e) {
        await storage.delete(_domainsKey);
      }
    }
    str = await storage.read(_indexKey);
    if ((str != null && str.isNotEmpty)) {
      try {
        final list = json.decode(str);
        _hostSet = Set<String>.from(list);
      } catch (e) {
        await storage.delete(_indexKey);
      }
    } else {
      _hostSet = <String>{};
    }
    _initialized = true;
  }

  @override
  Future<List<Cookie>> loadForRequest(Uri uri) async {
    await _checkInitialized();
    await _load(uri);
    return super.loadForRequest(uri);
  }

  @override
  Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {
    await _checkInitialized();
    if (cookies.isNotEmpty) {
      await super.saveFromResponse(uri, cookies);
      if (cookies.every((Cookie e) => e.domain == null)) {
        await _save(uri);
      } else {
        await _save(uri, true);
      }
    }
  }

  Map<String, Map<String, SerializableCookie>> _filter(
      Map<String, Map<String, SerializableCookie>> domain,
      ) {
    return domain.cast<String, Map<String, dynamic>>().map(
          (path, cookies) {
        final result = cookies.cast<String, SerializableCookie>().map(
              (key, cookie) {
            final isSession =
                cookie.cookie.expires == null && cookie.cookie.maxAge == null;
            if ((isSession && persistSession) ||
                (persistSession && !cookie.isExpired())) {
              return MapEntry(key, cookie);
            } else {
              return MapEntry(null, cookie);
            }
          },
        )..removeWhere((k, v) => k == null);
        return MapEntry(path, result.cast<String, SerializableCookie>());
      },
    );
  }

  /// Delete cookies for specified [uri].
  /// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
  ///
  /// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
  @override
  Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
    await _checkInitialized();
    await super.delete(uri, withDomainSharedCookie);
    final host = uri.host;
    if (_hostSet.remove(host)) {
      await storage.write(_indexKey, json.encode(_hostSet.toList()));
    }
    await storage.delete(host);
    if (withDomainSharedCookie) {
      await storage.write(_domainsKey, json.encode(domainCookies));
    }
  }

  /// Delete all cookies files in the [storage] and the memory.
  @override
  Future<void> deleteAll() async {
    await _checkInitialized();
    await super.deleteAll();
    final keys = _hostSet.toList(growable: true)
      ..addAll([_indexKey, _domainsKey]);
    await storage.deleteAll(keys);
    _hostSet.clear();
  }

  Future<void> _save(Uri uri, [bool withDomainSharedCookie = false]) async {
    final host = uri.host;
    if (!_hostSet.contains(host)) {
      _hostSet.add(host);
      await storage.write(_indexKey, json.encode(_hostSet.toList()));
    }
    final cookies = hostCookies[host];
    if (cookies != null) {
      await storage.write(host, json.encode(_filter(cookies)));
    }
    if (withDomainSharedCookie) {
      final filterDomainCookies = domainCookies.map(
            (key, value) => MapEntry(key, _filter(value)),
      );
      await storage.write(_domainsKey, json.encode(filterDomainCookies));
    }
  }

  Future<void> _load(Uri uri) async {
    final host = uri.host;
    if (!_hostSet.contains(host) || hostCookies[host] != null) {
      return;
    }
    final str = await storage.read(host);
    if (str == null || str.isEmpty) {
      return;
    }
    try {
      final cookies = json.decode(str).cast<String, Map<String, dynamic>>();
      cookies.forEach((String path, Map<String, dynamic> map) {
        map.forEach((String k, dynamic v) {
          map[k] = SerializableCookie.fromJson(v);
        });
      });
      hostCookies[host] =
          cookies.cast<String, Map<String, SerializableCookie>>();
    } catch (e) {
      if (deleteHostCookiesWhenLoadFailed) {
        await storage.delete(host);
      }
      rethrow;
    }
  }
}


/// This class is a wrapper for `Cookie` class.
/// Because the `Cookie` class doesn't  support Json serialization,
/// for the sake of persistence, we use this class instead of it.
class SerializableCookie {
  SerializableCookie(this.cookie) {
    createTimeStamp = (DateTime.now().millisecondsSinceEpoch ~/ 1000).toInt();
  }

  final Cookie cookie;
  int createTimeStamp = 0;

  /// Create an instance from JSON string.
  factory SerializableCookie.fromJson(String value) {
    final t = value.split(';_crt=');
    return SerializableCookie(_Cookie.fromSetCookieValue(t[0]))
      ..createTimeStamp = int.parse(t[1]);
  }

  /// Tells whether this cookie is expired.
  bool isExpired() {
    final now = DateTime.now();
    return (cookie.maxAge != null && cookie.maxAge! < 1) ||
        (cookie.maxAge != null &&
            (now.millisecondsSinceEpoch ~/ 1000).toInt() - createTimeStamp >=
                cookie.maxAge!) ||
        (cookie.expires != null && !cookie.expires!.isAfter(now));
  }

  /// Serialize the JSON string.
  String toJson() => toString();

  @override
  String toString() => '$cookie;_crt=$createTimeStamp';
}

/// [DefaultCookieJar] is a default cookie manager which implements the standard
/// cookie policy declared in RFC.
///
/// [DefaultCookieJar] saves the cookies in the memory, all cookies will be
/// cleared after the app exited.
///
/// In order to save cookies into storages, use [PersistCookieJar] instead.
class DefaultCookieJar implements CookieJar {
  DefaultCookieJar({this.ignoreExpires = false});

  @override
  final bool ignoreExpires;

  /// An array to save cookies.
  ///
  /// [domains[0]] save the cookies with "domain" attribute.
  /// These cookie usually need to be shared among multiple domains.
  ///
  /// [domains[1]] save the cookies without "domain" attribute.
  /// These cookies are private for each host name.
  final List<
      Map<
          String, //domain or host
          Map<
              String, //path
              Map<
                  String, //cookie name
                  SerializableCookie //cookie
              >>>> _cookies =
  <Map<String, Map<String, Map<String, SerializableCookie>>>>[
    <String, Map<String, Map<String, SerializableCookie>>>{},
    <String, Map<String, Map<String, SerializableCookie>>>{},
  ];

  Map<String, Map<String, Map<String, SerializableCookie>>> get domainCookies =>
      _cookies[0];

  Map<String, Map<String, Map<String, SerializableCookie>>> get hostCookies =>
      _cookies[1];

  /// If you set Path=/docs, these request paths match:
  ///     /docs
  ///     /docs/
  ///     /docs/Web/
  ///     /docs/Web/HTTP
  /// But these request paths don't:
  ///     /
  ///     /docsets
  ///     /fr/docs
  bool _isPathMatch(String urlPath, String cookiePath) {
    final urlPathLowerCase = urlPath.toLowerCase();
    final cookiePathLowerCase = cookiePath.toLowerCase();
    if ('/' == cookiePath || urlPathLowerCase == cookiePathLowerCase) {
      return true;
    }
    if (urlPathLowerCase.startsWith(cookiePathLowerCase)) {
      final temp = urlPathLowerCase.substring(
        cookiePathLowerCase.endsWith('/')
            ? cookiePathLowerCase.length - 1
            : cookiePathLowerCase.length,
      );
      return temp.startsWith('/');
    }
    return false;
  }

  /// If you set Domain=.mozilla.org, these request domains match:
  ///     mozilla.org
  ///     developer.mozilla.org
  /// But these request domains don't:
  ///     fakemozilla.org
  ///     mozilla.org.com
  bool _isDomainMatch(String urlDomain, String cookieDomain) {
    if (urlDomain == cookieDomain) {
      return true;
    }
    if (urlDomain.endsWith(cookieDomain)) {
      final temp =
      urlDomain.substring(0, urlDomain.length - cookieDomain.length);
      return temp.endsWith('.');
    }
    return false;
  }

  @override
  Future<List<Cookie>> loadForRequest(Uri uri) async {
    final list = <Cookie>[];
    final urlPath = uri.path;
    // Load cookies without "domain" attribute, include port.
    final hostname = uri.host;
    for (final domain in hostCookies.keys) {
      if (hostname == domain) {
        final cookies =
        hostCookies[domain]!.cast<String, Map<String, dynamic>>();
        // Sort by best match （longer path first）
        final keys = cookies.keys.toList()
          ..sort((a, b) => b.length.compareTo(a.length));
        for (final path in keys) {
          if (_isPathMatch(urlPath, path)) {
            final values = cookies[path]!;
            for (final key in values.keys) {
              final SerializableCookie cookie = values[key];
              if (_check(uri.scheme, cookie)) {
                // preserve cookies that with same name but in different paths when request (as Chrome);
                // eg(in request header): Cookie: a=1;  a=2;  a=3
                list.add(cookie.cookie);
              }
            }
          }
        }
      }
    }
    // Load cookies with "domain" attribute, Ignore port.
    domainCookies.forEach(
          (domain, cookies) {
        if (_isDomainMatch(uri.host, domain)) {
          cookies.forEach(
                (path, values) {
              if (_isPathMatch(urlPath, path)) {
                values.forEach((key, v) {
                  if (_check(uri.scheme, v)) {
                    list.add(v.cookie);
                  }
                });
              }
            },
          );
        }
      },
    );
    return list;
  }

  @override
  Future<void> saveFromResponse(Uri uri, List<Cookie> cookies) async {
    for (final cookie in cookies) {
      String? domain = cookie.domain;
      String path;
      final int index;
      if (domain != null) {
        index = 0;
        // Save cookies with "domain" attribute.
        if (domain.startsWith('.')) {
          domain = domain.substring(1);
        }
        path = cookie.path ?? '/';
      } else {
        // Save cookies without "domain" attribute.
        index = 1;
        domain = uri.host;
        path = cookie.path ?? _curDir(uri.path);
      }
      final mapDomain =
          _cookies[index][domain]?.cast<String, Map<String, dynamic>>() ??
              <String, Map<String, dynamic>>{};
      final map = mapDomain[path] ?? <String, dynamic>{};
      map[cookie.name] = SerializableCookie(cookie);
      if (_isExpired(map[cookie.name])) {
        map.remove(cookie.name);
      }
      mapDomain[path] = map.cast<String, SerializableCookie>();
      _cookies[index][domain] =
          mapDomain.cast<String, Map<String, SerializableCookie>>();
    }
  }

  /// Delete cookies for specified [uri].
  /// This API will delete all cookies for the `uri.host`, it will ignored the `uri.path`.
  ///
  /// [withDomainSharedCookie] `true` will delete the domain-shared cookies.
  @override
  Future<void> delete(Uri uri, [bool withDomainSharedCookie = false]) async {
    final host = uri.host;
    hostCookies.remove(host);
    if (withDomainSharedCookie) {
      domainCookies.removeWhere(
            (domain, v) => _isDomainMatch(uri.host, domain),
      );
    }
  }

  /// Delete all cookies stored in the memory.
  @override
  Future<void> deleteAll() async {
    domainCookies.clear();
    hostCookies.clear();
  }

  bool _isExpired(SerializableCookie cookie) {
    return ignoreExpires ? false : cookie.isExpired();
  }

  bool _check(String scheme, SerializableCookie cookie) {
    return cookie.cookie.secure && scheme == 'https' || !_isExpired(cookie);
  }

  String _curDir(String path) {
    if (path.isEmpty) {
      return '/';
    }
    final list = path.split('/')..removeLast();
    return list.join('/');
  }
}